<script>
import { GlKeysetPagination } from '@gitlab/ui';
import { get, isEqual } from 'lodash';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants';
import VulnerabilityList from './vulnerability_list.vue';
import { FIELDS } from './constants';

export const PAGE_SIZE_STORAGE_KEY = 'vulnerability_list_page_size';

const GRAPHQL_DATA_PATH = {
  [DASHBOARD_TYPES.PROJECT]: 'project.vulnerabilities',
  [DASHBOARD_TYPES.GROUP]: 'group.vulnerabilities',
  [DASHBOARD_TYPES.INSTANCE]: 'vulnerabilities',
  [DASHBOARD_TYPES.PIPELINE]: 'project.pipeline.securityReportFindings',
};

export default {
  components: {
    VulnerabilityList,
    GlKeysetPagination,
    PageSizeSelector,
    LocalStorageSync,
  },
  mixins: [glFeatureFlagsMixin()],
  inject: {
    dashboardType: 'dashboardType',
    vulnerabilitiesQuery: 'vulnerabilitiesQuery',
    fullPath: 'fullPath',
    canViewFalsePositive: {
      default: false,
    },
    hasJiraVulnerabilitiesIntegrationEnabled: {
      default: false,
    },
    pipeline: {
      default: {},
    },
  },
  props: {
    filters: {
      type: Object,
      required: false,
      default: null,
    },
    fields: {
      type: Array,
      required: true,
    },
    showProjectNamespace: {
      type: Boolean,
      required: false,
      default: false,
    },
    portalName: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      vulnerabilities: [],
      pageInfo: {},
      // The "before" querystring value on page load.
      initialBefore: this.$route.query.before,
      pageSize: Number(localStorage.getItem(PAGE_SIZE_STORAGE_KEY) ?? DEFAULT_PAGE_SIZE),
    };
  },
  apollo: {
    vulnerabilities: {
      query() {
        return this.vulnerabilitiesQuery;
      },
      context: {
        // Marks the query to be a single request to reduce the query's complexity score
        // https://docs.gitlab.com/ee/api/graphql/index.html#max-query-complexity
        // https://docs.gitlab.com/ee/development/fe_guide/graphql.html#managing-performance
        isSingleRequest: true,
      },
      errorPolicy: 'none',
      variables() {
        return this.queryVariables;
      },
      update(data) {
        const vulnerabilities = this.getVulnerabilitiesFromData(data);
        this.pageInfo = vulnerabilities.pageInfo;

        return vulnerabilities.nodes;
      },
      error() {
        createAlert({
          message: s__(
            'SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again.',
          ),
        });
      },
      skip() {
        return !this.filters;
      },
    },
  },
  computed: {
    before: {
      get() {
        // The GraphQL query can only have a "before" or an "after" but not both, so if both are in
        // the querystring, we'll only use "after" and pretend the "before" doesn't exist.
        return this.after ? undefined : this.$route.query.before;
      },
      set(before) {
        // Check if before will change, otherwise Vue Router will throw a "you're navigating to the
        // same route" error.
        if (before !== this.before) {
          this.pushQuerystring({ before, after: undefined });
        }
      },
    },
    after: {
      get() {
        return this.$route.query.after;
      },
      set(after) {
        // Check if after will change, otherwise Vue Router will throw a "you're navigating to the
        // same route" error.
        if (after !== this.after) {
          this.pushQuerystring({ before: undefined, after });
        }
      },
    },
    sort: {
      get() {
        return {
          sortBy: this.$route.query.sortBy || FIELDS.SEVERITY.key,
          sortDesc: this.$route.query.sortDesc ? parseBoolean(this.$route.query.sortDesc) : true,
        };
      },
      set({ sortBy, sortDesc }) {
        this.pushQuerystring({ sortBy, sortDesc });
      },
    },
    isLoadingVulnerabilities() {
      return this.$apollo.queries.vulnerabilities.loading;
    },
    queryVariables() {
      return {
        fullPath: this.fullPath,
        sort: `${convertToSnakeCase(this.sort.sortBy)}_${this.sort.sortDesc ? 'desc' : 'asc'}`,
        vetEnabled: this.canViewFalsePositive,
        includeExternalIssueLinks: this.hasJiraVulnerabilitiesIntegrationEnabled,
        pipelineId: this.pipeline.iid,
        // If we're using "after" we need to use "first", and if we're using "before" we need to
        // use "last". See this comment for more info:
        // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79834#note_831878506
        first: this.before ? null : this.pageSize,
        last: this.before ? this.pageSize : null,
        before: this.before,
        after: this.after,
        ...this.filters,
      };
    },
  },
  watch: {
    filters(newFilters, oldFilters) {
      // The first time the filters are set, it's done by the vulnerability-filters component, so
      // we don't want to reset the paging. Every time after that will be from user interaction.
      if (oldFilters !== null) {
        this.resetPaging();
      }
    },
    sort(newSort, oldSort) {
      // Anything that changes the querystring will also trigger this watcher (because sort is
      // recomputed), so we need to check that the sort has changed before we reset the paging.
      if (newSort.sortBy !== oldSort.sortBy || newSort.sortDesc !== oldSort.sortDesc) {
        this.resetPaging();
      }
    },
    queryVariables(newVariables, oldVariables) {
      // For some reason queryVariables is called twice, but the first time the variables won't
      // change. We'll only emit the event the second time after the variables have changed.
      if (!isEqual(newVariables, oldVariables)) {
        this.$emit('query-variables-changed', newVariables);
      }
    },
  },
  methods: {
    getNextPage() {
      this.after = this.pageInfo.endCursor || this.before;
    },
    getPrevPage() {
      this.before = this.pageInfo.startCursor || this.after;
    },
    resetPaging() {
      this.before = undefined;
      this.after = undefined;
    },
    pushQuerystring(data) {
      this.$router.push({ query: { ...this.$route.query, ...data } });
    },
    getVulnerabilitiesFromData(data) {
      return get(data, GRAPHQL_DATA_PATH[this.dashboardType]);
    },
  },
  PAGE_SIZE_STORAGE_KEY,
};
</script>

<template>
  <div>
    <vulnerability-list
      :is-loading="isLoadingVulnerabilities"
      :vulnerabilities="vulnerabilities"
      :fields="fields"
      :sort.sync="sort"
      :page-size="pageSize"
      :should-show-project-namespace="showProjectNamespace"
      :portal-name="portalName"
      @vulnerability-clicked="$emit('vulnerability-clicked', $event)"
    />

    <div class="gl-text-center gl-mt-6 gl-relative">
      <gl-keyset-pagination
        :has-previous-page="pageInfo.hasPreviousPage"
        :has-next-page="pageInfo.hasNextPage"
        :start-cursor="pageInfo.startCursor"
        :end-cursor="pageInfo.endCursor"
        :disabled="isLoadingVulnerabilities"
        @next="getNextPage"
        @prev="getPrevPage"
      />

      <local-storage-sync v-model="pageSize" :storage-key="$options.PAGE_SIZE_STORAGE_KEY">
        <page-size-selector v-model="pageSize" class="gl-absolute gl-right-0" />
      </local-storage-sync>
    </div>
  </div>
</template>
